package io.hepu.robotize.util;

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;

import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.*;

public final class Reflects {

    /**
     * Returns all fields in target {@code Class} or it's super {@code Class}
     * includes public, protected, default, private
     *
     * @param cls target class
     * @return array of Fields
     */
    public static List<Field> getAllFields(final Class<?> cls) {
        final List<Field> allFields = new ArrayList<>();
        Class<?> currentClass = cls;
        while (currentClass != null) {
            final Field[] declaredFields = currentClass.getDeclaredFields();
            allFields.addAll(Arrays.asList(declaredFields));
            currentClass = currentClass.getSuperclass();
        }
        return allFields;
    }

    /**
     * Returns all public fields in target {@code Class} or it's super {@code Class}
     *
     * @param cls target class
     * @return array of Fields
     */
    public static Field[] getPublicFields(final Class<?> cls) {
        return cls.getFields();
    }

    /**
     * Returns {@code Class} all fields includes public, protected, default, private
     *
     * @param cls target class
     * @return array of Fields
     */
    public static Field[] getOwnFields(final Class<?> cls) {
        return cls.getDeclaredFields();
    }

    public static Field getField(final Class<?> cls, String fieldName) {
        final List<Field> fields = getAllFields(cls);
        for (Field field : fields) {
            if (field.getName().equals(fieldName)) {
                return field;
            }
        }
        return null;
    }

    /**
     * Gets the {@code Field} value by specified the field name on target {@code Object} instance.
     *
     * @param inst      instance of class
     * @param fieldName file name
     * @return value on field
     */
    public static <T> Object get(T inst, String fieldName) {
        Field field = getField(inst.getClass(), fieldName);
        if (field == null) {
            return null;
        }
        field.setAccessible(true);
        try {
            return field.get(inst);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * Sets the value to {@code Field} by specified the field name on target {@code Object} instance.
     *
     * @param inst      instance of class
     * @param fieldName file name
     * @param value     the value to set
     */
    public static <T> void set(T inst, String fieldName, Object value) {
        Field field = getField(inst.getClass(), fieldName);
        if (field == null) {
            return;
        }
        field.setAccessible(true);
        try {
            field.set(inst, value);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public static <T> void orSet(T inst, String fieldName, Object value) {
        Object original = get(inst, fieldName);
        if (Limits.isEmpty(original)) {
            set(inst, fieldName, value);
        }
    }

    /**
     * Gets null property names.
     *
     * @param source the object wrapped by this BeanWrapper
     * @return null property names of the object wrapped
     */
    public static String[] getNullNames(Object source) {
        final BeanWrapper src = new BeanWrapperImpl(source);
        PropertyDescriptor[] pds = src.getPropertyDescriptors();
        Set<String> names = new HashSet<>();
        for (PropertyDescriptor pd : pds) {
            Object value = src.getPropertyValue(pd.getName());
            if (value == null) {
                names.add(pd.getName());
            }
        }
        String[] result = new String[names.size()];
        return names.toArray(result);
    }

    public static String[] getNonNullNames(Object source) {
        final BeanWrapper src = new BeanWrapperImpl(source);
        PropertyDescriptor[] pds = src.getPropertyDescriptors();
        Set<String> names = new HashSet<>();
        for (PropertyDescriptor pd : pds) {
            Object value = src.getPropertyValue(pd.getName());
            if (value != null) {
                names.add(pd.getName());
            }
        }
        String[] result = new String[names.size()];
        return names.toArray(result);
    }

    /**
     * Gets annotation of field
     *
     * @param field           class field
     * @param annotationClass annotation class
     * @param <T>             annotation type
     * @return annotation instance
     */
    public static <T extends Annotation> T getFieldAnnotation(Field field, Class<T> annotationClass) {
        if (annotationClass == null) {
            return null;
        }
        return field.getAnnotation(annotationClass);
    }

    public static <T extends Annotation> T getFieldAnnotation(Class<?> cls, String fieldName, Class<T> annotationClass) {
        Field field = getField(cls, fieldName);
        return field != null ? getFieldAnnotation(field, annotationClass) : null;
    }

    public static <T extends Annotation> T[] getClassAnnotations(Class<?> cls, Class<T> annotationClass) {
        if (annotationClass == null) {
            return null;
        }
        return cls.getAnnotationsByType(annotationClass);
    }

    public static <T extends Annotation> T getClassAnnotation(Class<?> cls, Class<T> annotationClass) {
        if (annotationClass == null) {
            return null;
        }
        return cls.getAnnotation(annotationClass);
    }
}
